/********************************************************************************
 * Copyright (c) 2019 [Open Lowcode SAS](https://openlowcode.com/)
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0 .
 *
 * SPDX-License-Identifier: EPL-2.0
 ********************************************************************************/

package org.openlowcode.server.data.properties;

import java.util.ArrayList;

import java.util.logging.Logger;

import org.openlowcode.tools.misc.NamedList;
import org.openlowcode.module.system.data.sequence.ObjectidseedSequence;
import org.openlowcode.server.data.DataObject;
import org.openlowcode.server.data.DataObjectPayload;
import org.openlowcode.server.data.DataObjectProperty;
import org.openlowcode.server.data.QueryHelper;
import org.openlowcode.server.data.formula.DataUpdateTrigger;
import org.openlowcode.server.data.formula.TriggerLauncher;
import org.openlowcode.server.data.storage.AndQueryCondition;
import org.openlowcode.server.data.storage.LimitedFieldsUpdateQuery;
import org.openlowcode.server.data.storage.OrQueryCondition;
import org.openlowcode.server.data.storage.QueryCondition;
import org.openlowcode.server.data.storage.SimpleEqualQueryCondition;
import org.openlowcode.server.data.storage.StoredFieldSchema;
import org.openlowcode.server.runtime.OLcServer;

/**
 * The unique identified property adds to an object a unique id generated by the
 * server. Objects with the unique identified property can be queried one by
 * one. All objects except potentially some logs that are persisted should have
 * a Uniqueidentified property. The Id field is actually stored by the related
 * Hasid property
 * 
 * @author <a href="https://openlowcode.com/" rel="nofollow">Open Lowcode
 *         SAS</a>
 *
 * @param <E>
 */
public class Uniqueidentified<E extends DataObject<E>>
		extends
		DataObjectProperty<E> {
	private static Logger logger = Logger.getLogger(Uniqueidentified.class.getName());
	private Storedobject<E> storedobject;
	private Hasid<E> hasid;

	private static long currentseed = -1;
	private static long currentincrement = -1;

	private static final long MAX_INCREMENT = 1024;

	/**
	 * a central method to the server that will generate unique numbers. It will use
	 * the result of a sequence in the database, to get the unique number base, and
	 * will add a memory generated suffix between 1 and 1024. The method can support
	 * several servers connecting in parallel to the same database for horizontal
	 * stability
	 * 
	 * @return the next unique id
	 */
	protected static synchronized long getNextId() {
		// reinitiates new seed if never initiated or if increment for seed is over.
		if ((currentseed == -1) || (currentincrement == MAX_INCREMENT)) {
			currentseed = ObjectidseedSequence.get().getNextValue();
			currentincrement = 0;
		}
		long returnvalue = currentseed * 1024 + currentincrement;
		currentincrement++;
		return returnvalue;
	}

	/**
	 * @return the definition for this property
	 */
	public UniqueidentifiedDefinition<E> getDefinition() {
		return (UniqueidentifiedDefinition<E>) this.definition;
	}

	/**
	 * Creates the property for an object
	 * 
	 * @param definition    definition
	 * @param parentpayload the payload of the parent data object
	 */

	public Uniqueidentified(UniqueidentifiedDefinition<E> definition, DataObjectPayload parentpayload) {
		super(definition, parentpayload);

	}

	/**
	 * performs an efficient update of several objects
	 * 
	 * @param objectbatch           the list of objects
	 * @param uniqueidentifiedbatch their unique identified property (has to be the
	 *                              same size than the object batch)
	 */
	public static <E extends DataObject<E> & UniqueidentifiedInterface<E>> void update(
			E[] objectbatch,
			Uniqueidentified<E>[] uniqueidentifiedbatch) {
		if (objectbatch == null)
			throw new RuntimeException("cannot treat null array");
		if (uniqueidentifiedbatch == null)
			throw new RuntimeException("cannot treat null array of uniqueidentified");
		QueryCondition[] conditions = new QueryCondition[objectbatch.length];
		DataObjectPayload[] payloads = new DataObjectPayload[objectbatch.length];
		for (int i = 0; i < objectbatch.length; i++) {
			Uniqueidentified<E> uniqueidentified = uniqueidentifiedbatch[i];
			QueryCondition objectuniversalcondition = uniqueidentified.definition.getParentObject()
					.getUniversalQueryCondition(uniqueidentified.definition, null);
			QueryCondition uniqueidcondition = HasidQueryHelper.getIdQueryCondition(null,
					uniqueidentified.getRelatedHasid().getId().getId(), uniqueidentified.definition.getParentObject());
			QueryCondition finalcondition = uniqueidcondition;
			if (objectuniversalcondition != null) {
				finalcondition = new AndQueryCondition(objectuniversalcondition, uniqueidcondition);
			}
			payloads[i] = uniqueidentified.parentpayload;
			conditions[i] = finalcondition;
		}
		DataObjectPayload.massiveupdate(payloads, conditions);
	}

	/**
	 * persists all changes done to this object in-memory version into the
	 * persistence layer
	 * 
	 * @param object the object to update
	 */
	public void update(E object) {
		QueryCondition objectuniversalcondition = definition.getParentObject().getUniversalQueryCondition(definition,
				null);
		QueryCondition uniqueidcondition = HasidQueryHelper.getIdQueryCondition(null, this.hasid.getId().getId(),
				definition.getParentObject());
		QueryCondition finalcondition = uniqueidcondition;
		if (objectuniversalcondition != null) {
			finalcondition = new AndQueryCondition(objectuniversalcondition, uniqueidcondition);
		}
		NamedList<DataUpdateTrigger<E>> triggers = object.getDataUpdateTriggers();
		TriggerLauncher<E> triggerlauncher = new TriggerLauncher<E>(triggers);
		triggerlauncher.executeTriggerList(object);

		parentpayload.update(finalcondition);

	}

	/**
	 * performs a similar action to the update method, except it will be registered
	 * as a refresh, typically meaning the business data was not changed manually by
	 * the user. Has a distinct list of triggers from the update
	 * 
	 * @param object the object to refresh
	 */
	public void refresh(E object) {
		QueryCondition objectuniversalcondition = definition.getParentObject().getUniversalQueryCondition(definition,
				null);
		QueryCondition uniqueidcondition = HasidQueryHelper.getIdQueryCondition(null, this.hasid.getId().getId(),
				definition.getParentObject());
		QueryCondition finalcondition = uniqueidcondition;
		if (objectuniversalcondition != null) {
			finalcondition = new AndQueryCondition(objectuniversalcondition, uniqueidcondition);
		}
		NamedList<DataUpdateTrigger<E>> triggers = object.getDataRefreshTriggers();
		TriggerLauncher<E> triggerlauncher = new TriggerLauncher<E>(triggers);
		triggerlauncher.executeTriggerList(object);
		parentpayload.update(finalcondition);

	}

	/**
	 * this method will generate the id before insertion
	 * 
	 * @param object the object
	 */
	public void preprocStoredobjectInsert(E object) {
		if (this.hasid.getId().getId() != null)
			if (this.hasid.getId().getId().length() > 0)
				throw new RuntimeException("Try to insert an already persisted object " + object.getName() + " ID = "
						+ this.hasid.getId().getId());
		long idnumber = Uniqueidentified.getNextId();
		// first char in screen indicates the algorithm. Before Open Lowcode v0.33,
		// string started by 1 as algorithm based on getTime
		// 2 indicates an algorithm based on the integer sequence seed (2 billion values
		// for seed + 1024 per round). Encoding in hex as more readable than base64
		String idstring = "2" + Long.toHexString(idnumber);
		// NOTE: due to path algorithm, it is assumed the id only contains letters and
		// figures (especially "[", "]" and "/" are forbidden

		this.hasid.SetId(idstring);
		this.hasid.setDeleted("N");

	}

	/**
	 * This method allows to set the dependent property stored object. It may be
	 * used by some methods of this property
	 * 
	 * @param storedobject the dependent stored object property
	 */
	public void setDependentPropertyStoredobject(Storedobject<E> storedobject) {
		this.storedobject = storedobject;

	}

	/**
	 * This method allows to set the dependent property stored object. It may be
	 * used by some methods of this property
	 * 
	 * @param hasid the dependent has id property
	 * @since 2.0
	 */
	public void setDependentPropertyHasid(Hasid<E> hasid) {
		this.hasid = hasid;
	}

	private String generateDeleteLogForObject(E object) {
		return "DELETING " + this.getDefinition().getParentObject().getName() + " as "
				+ OLcServer.getServer().getCurrentUser().getNr() + ", ID=" + this.hasid.getId().getId() + ", "
				+ object.dropToString();
	}

	/**
	 * Deletes the object. The object is removed from the persistence layer.
	 * However, a trace is put in the logs for further reference (at severe level)
	 * 
	 * @param object the object to delete
	 */
	@SuppressWarnings("unchecked")
	public void delete(E object) {
		logger.severe(generateDeleteLogForObject(object));
		QueryCondition uniqueidcondition = HasidQueryHelper.getIdQueryCondition(null, this.hasid.getId().getId(),
				definition.getParentObject());
		LimitedFieldsUpdateQuery limitedupdatequery = new LimitedFieldsUpdateQuery(
				definition.getParentObject().getTableschema(), uniqueidcondition);
		limitedupdatequery.addFieldUpdate(new SimpleEqualQueryCondition<String>(null,
				(StoredFieldSchema<String>) (this.hasid.getDefinition().getDefinition().lookupOnName("DELETED")), "Y"));
		QueryHelper.getHelper().limitedUpdate(limitedupdatequery);

	}

	/**
	 * generates by batch unique id for the object before they are inserted
	 * 
	 * @param object                       a batch of object
	 * @param preprocuniqueidentifiedbatch the corresponding unique identified
	 *                                     properties
	 */
	public static <E extends DataObject<E>> void preprocStoredobjectInsert(
			E[] object,
			Uniqueidentified<E>[] preprocuniqueidentifiedbatch) {

		if (object == null)
			throw new RuntimeException("object batch is null");
		if (preprocuniqueidentifiedbatch == null)
			throw new RuntimeException("creationlog batch is null");
		if (object.length != preprocuniqueidentifiedbatch.length)
			throw new RuntimeException("Object batch length " + object.length
					+ " is not consistent with creationlog batch length " + preprocuniqueidentifiedbatch.length);

		// batch control that it is first insertion. Else, the entire batch fails.
		for (int i = 0; i < preprocuniqueidentifiedbatch.length; i++) {
			Uniqueidentified<E> thisuniqueidentified = preprocuniqueidentifiedbatch[i];
			if (thisuniqueidentified.hasid.getId().getId() != null)
				if (thisuniqueidentified.hasid.getId().getId().length() > 0)
					throw new RuntimeException("Try to insert an already persisted object " + object[0].getName()
							+ " inside batch, ID = " + thisuniqueidentified.hasid.getId().getId());
		}
		// batch control that it is first insertion. Else, the entire batch fails.
		for (int i = 0; i < preprocuniqueidentifiedbatch.length; i++) {
			Uniqueidentified<E> thisuniqueidentified = preprocuniqueidentifiedbatch[i];
			thisuniqueidentified.preprocStoredobjectInsert(object[i]);
		}

	}

	/**
	 * @return the related property has id
	 * @since 2.0
	 */
	public Hasid<E> getRelatedHasid() {
		return this.hasid;
	}

	/**
	 * @return the related property stored object
	 */
	public Storedobject<E> getRelatedStoredobject() {
		return this.storedobject;
	}

	/**
	 * performs a delete by batch
	 * 
	 * @param object                         an array of object
	 * @param uniqueidentifiedarrayformethod their corresponding unique identified
	 *                                       properties
	 */
	@SuppressWarnings("unchecked")
	public static <
			E extends DataObject<E>> void delete(E[] object, Uniqueidentified<E>[] uniqueidentifiedarrayformethod) {
		if (object == null)
			throw new RuntimeException("cannot treat null array");
		if (uniqueidentifiedarrayformethod == null)
			throw new RuntimeException("cannot treat null array of uniqueidentified");
		if (object.length != uniqueidentifiedarrayformethod.length)
			throw new RuntimeException("Uniqueidentified Array and Object Array do not have same size");
		// do nothing if no line in input
		if (object.length==0) return;
		
		StringBuffer deleteobjectlog = new StringBuffer();
		ArrayList<QueryCondition> conditionlist = new ArrayList<QueryCondition>();
		for (int i = 0; i < object.length; i++) {

			Uniqueidentified<E> uniqueidentified = uniqueidentifiedarrayformethod[i];
			deleteobjectlog.append("			" + uniqueidentified.generateDeleteLogForObject(object[i]) + "\n");
			if (i % 100 == 99) {
				logger.severe(" ---- Multiple row delete log, details in next line for lines " + i + "---- \n"
						+ deleteobjectlog.toString());
				deleteobjectlog = new StringBuffer();
			}

			QueryCondition uniqueidcondition = HasidQueryHelper.getIdQueryCondition(null,
					uniqueidentified.getRelatedHasid().getId().getId(), uniqueidentified.definition.getParentObject());
		
			conditionlist.add(uniqueidcondition);
		
		}
		logger.severe(" ---- Multiple row delete log, details in next line ---- \n" + deleteobjectlog.toString());
		OrQueryCondition allidscondition = new OrQueryCondition();
		for (int i=0;i<conditionlist.size();i++) allidscondition.addCondition(conditionlist.get(i));
		
		LimitedFieldsUpdateQuery limitedupdatequery = new LimitedFieldsUpdateQuery(
				uniqueidentifiedarrayformethod[0].definition.getParentObject().getTableschema(), allidscondition);
		
		limitedupdatequery.addFieldUpdate(new SimpleEqualQueryCondition<String>(null,
				(StoredFieldSchema<String>) (uniqueidentifiedarrayformethod[0].hasid.getDefinition().getDefinition().lookupOnName("DELETED")), "Y"));
		QueryHelper.getHelper().limitedUpdate(limitedupdatequery);


	}

}
